classdef TestDotsQueryable < dotsTestCase
    
    properties
        queryable;
        group;
    end
    
    methods
        function self = TestDotsQueryable(name)
            self = self@dotsTestCase(name);
        end
        
        function setUp(self)
            self.setUp@dotsTestCase;
            self.queryable = dotsQueryableTestable;
            self.group = 'test group';
        end
        
        function tearDown(self)
            self.tearDown@dotsTestCase;
        end
        
        function testReadData(self)
            self.queryable.appendData(self.queryable.makeNewData);
            earlyTime = self.queryable.latestDataTime;
            
            state = self.queryable.currentState;
            assertTrue(all(isfinite(state(1:end))), ...
                'current state should reflect new finite data')
            assertFalse(isempty(self.queryable.allData), ...
                'should have new data')
            
            self.queryable.appendData(self.queryable.makeNewData);
            lateTime = self.queryable.latestDataTime;
            firstTime = self.queryable.earliestDataTime;
            assertTrue(lateTime > earlyTime, ...
                'should account for last data arrival times')
            assertTrue(earlyTime >= firstTime, ...
                'should account for first data arrival times')
        end
        
        function testQuerySingleDimClassifications(self)
            % contrive a phenomenon for the max value of each dimension
            % make a classificatin for each phenomenon
            % Since the neutral values of dotsQueryableTestable are the
            % mins:
            %   before adding maxed data, should get a trivial query
            %   output
            %   after adding maxed data, should get a non-trivial output
            
            temp = self.queryable.getPhenomenonTemplate;
            for ii = 1:self.queryable.nDimensions
                phenom = self.queryable.phenomenons{'max'}{sprintf('%d',ii)};
                expectedOutput = phenom;
                self.queryable.addClassificationInGroupWithRank( ...
                    phenom, expectedOutput, self.group, ii);
                self.queryable.activeClassificationGroup = self.group;
                
                self.queryable.flushData;
                output = self.queryable.queryAsOfTime;
                % trivial test is meaningless for single-valued dimension
                if self.queryable.dimensions(ii).size > 1
                    assertFalse(isequal(output, expectedOutput), ...
                        'with no relevant data, query should give trivial output')
                end
                
                % get data to hit this phenomenon
                self.queryable.appendData(self.queryable.makeNewData('max'));
                output = self.queryable.queryAsOfTime;
                assertTrue(isequal(output, expectedOutput), ...
                    'with relevant data, query should give specified output')
            end
        end
        
        function testQueryMultiDimClassifications(self)
            % contrive a phenomenon: all dimensions at maximum
            phenoms = self.queryable.phenomenons{'max'};
            corner = dotsPhenomenon.composite([phenoms{:}], 'intersection');
            assertEqual(1, dotsPhenomenon.getSize(corner), ...
                'maxed-out corner of phenomenon space should have size 1');
            
            % add a query classification for the phenomenon
            expectedOutput = corner;
            self.queryable.addClassificationInGroupWithRank( ...
                corner, expectedOutput, self.group, 1);
            self.queryable.activeClassificationGroup = self.group;
            
            % get a trivial output without any data
            output = self.queryable.queryAsOfTime;
            assertFalse(isequal(output, expectedOutput), ...
                'with no relevant data, query should give trivial output')
            
            % get a trivial output with irrelevant data
            %   - each dimension at minimum
            self.queryable.appendData(self.queryable.makeNewData('min'));
            output = self.queryable.queryAsOfTime;
            assertFalse(isequal(output, expectedOutput), ...
                'with irrelevant data, query should give trivial output')
            
            % get a trivial output with partially relevant data
            %   all but one dimension at maximum
            partialMax = self.queryable.makeNewData('max');
            partialMax(end,2) = -inf;
            self.queryable.appendData(partialMax);
            output = self.queryable.queryAsOfTime;
            assertFalse(isequal(output, expectedOutput), ...
                'with partial relevant data, query should give trivial output')
            
            % get a non-trivial output with fully relevant data
            %   all dimensions at maximum
            self.queryable.appendData(self.queryable.makeNewData('max'));
            output = self.queryable.queryAsOfTime;
            assertEqual(output, expectedOutput, ...
                'with relevant data, query should give specified output')
        end
        
        function testQueryAsOfPastTime(self)
            % look for max on one dimension
            dim = self.queryable.nTestDims;
            phenom = self.queryable.phenomenons{'max'}{sprintf('%d',dim)};
            expectedOutput = phenom;
            self.queryable.addClassificationInGroupWithRank( ...
                phenom, expectedOutput, self.group, 1);
            self.queryable.activeClassificationGroup = self.group;
            
            % add relevant and irrelevant data at different times
            self.queryable.appendData(self.queryable.makeNewData('min'));
            irrelevantTime = self.queryable.latestDataTime;
            self.queryable.appendData(self.queryable.makeNewData('max'));
            relevantTime = self.queryable.latestDataTime;
            
            output = self.queryable.queryAsOfTime(irrelevantTime);
            assertFalse(isequal(output, expectedOutput), ...
                'at irrelevant time, query should give trivial output')
            
            output = self.queryable.queryAsOfTime(relevantTime);
            assertEqual(output, expectedOutput, ...
                'at relevant time, query should give specified output')
        end
        
        function testQueryOutputRanking(self)
            % look for max on one dimension
            dim = self.queryable.nTestDims;
            phenom = self.queryable.phenomenons{'max'}{sprintf('%d',dim)};
            expectedOutput = phenom;
            self.queryable.addClassificationInGroupWithRank( ...
                phenom, expectedOutput, self.group, 1);
            self.queryable.activeClassificationGroup = self.group;
            
            % make two classification for the same phenomenon but with
            % different ranks
            expectedOutput = 'high rank';
            self.queryable.addClassificationInGroupWithRank( ...
                phenom, expectedOutput, self.group, 100);
            wrongOutput = 'low rank';
            self.queryable.addClassificationInGroupWithRank( ...
                phenom, wrongOutput, self.group, 0);
            self.queryable.activeClassificationGroup = self.group;
            
            % get a hit on the higher-ranked classification.
            self.queryable.appendData(self.queryable.makeNewData('max'));
            output = self.queryable.queryAsOfTime;
            assertEqual(output, expectedOutput, ...
                'query output should match the higher-ranked classification')
        end
        
        function testDimensionTracking(self)
            % look for max on one dimension
            dim = self.queryable.nTestDims;
            phenom = self.queryable.phenomenons{'max'}{sprintf('%d',dim)};
            expectedOutput = phenom;
            self.queryable.addClassificationInGroupWithRank( ...
                phenom, expectedOutput, self.group, 1);
            self.queryable.activeClassificationGroup = self.group;
            
            % track the dimension of interest
            self.queryable.isTrackingDimension(dim) = true;
            self.queryable.upToDateOutput = 'OK, up to date now';
            
            % add relevant and irrelevant data at different times
            self.queryable.appendData(self.queryable.makeNewData('min'));
            irrelevantTime = self.queryable.latestDataTime;
            self.queryable.appendData(self.queryable.makeNewData('max'));
            relevantTime = self.queryable.latestDataTime;
            
            expected = [irrelevantTime, relevantTime];
            assertEqual(expected, self.queryable.trackingTimestamps, ...
                'should have queued timestamps for tracked dimension');
            
            % run through queued timestamps
            output = self.queryable.queryNextTracked;
            assertFalse(isequal(output, expectedOutput), ...
                'irrelevant, tracked timestamp should give trivial output')
            
            output = self.queryable.queryNextTracked;
            assertEqual(output, expectedOutput, ...
                'relevant, tracked timestamp should give specified output')
            
            assertTrue(isempty(self.queryable.trackingTimestamps), ...
                'should have used up all tracking timestamps')
            
            output = self.queryable.queryNextTracked;
            assertEqual(output, self.queryable.upToDateOutput, ...
                'with no tracking timestamps, should return custom fallback')
        end
    end
end